#!/usr/bin/perl
#
# oszsnmpd - daemon of the SNMP poll interface for Zabbix
# Copyright (C) 2010 Open Sistemas <imasd@opensistemas.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#

use strict;
use warnings;

use Getopt::Std;
use Sys::Syslog;
use Sys::Hostname;
use YAML;
use DBI;

use NetSNMP::OID (':all');
use NetSNMP::agent (':all');
use NetSNMP::ASN (':all');

use OpenSistemas::OSZSNMP;
use OpenSistemas::OID::Item;
use OpenSistemas::Zabbix::Alarms;

use vars qw(%opts $app);

my $VERSION = "1.0";
my $PROG = "oszsnmpd";

my $usagetxt = qq {$PROG ver $VERSION

Usage:

 $PROG [OPTIONS] oszsnmpd.conf

Options:

  -h          This help screen
  -p file     PID file (default: /tmp/$PROG.PID)
  -d          Debug (very verbose, attached to the console)

};

sub pollData
{
  if ($app->polling())
  {
    syslog('err', 'Your polling interval is too short... adding 15 seconds');
    $app->pollTimeAdd(15);
    return;
  }
  # we're polling, prevent reentrance
  $app->polling(1);

  syslog('debug', "DEBUG: polling data") if $opts{d};
  $app->update();

  syslog('debug', "DEBUG: data needs sorting") if $opts{d} && !$app->sorted();
  $app->sort();

  syslog('debug', "DEBUG: polling data done, see you in " .$app->pollTime() ." secs") if $opts{d};
  $app->polling(0);
  alarm $app->pollTime();
}

sub snmpHandler
{
  my ($self, $regi, $reqi, $reqs) = @_;
  my $req;

  print STDERR "DEBUG: entering snmpHandler...\n" if $opts{d};
  for($req = $reqs; $req; $req = $req->next())
  {
    my $oid = $req->getOID();
    print STDERR 'req OID:' .$oid ."\n" if $opts{d};

    if ($reqi->getMode() == MODE_GET)
    {
      print STDERR "DEBUG: MODE is GET\n" if $opts{d};
      print STDERR "MODE is GET\n" if !$opts{d};
      my $item = $app->getItem($oid);
      if($item)
      {
        my $value = defined $item->value() ? $item->value() : '';
        print STDERR "DEBUG:    " .$item->to_string() .", $value\n" if $opts{d};

        print STDERR  $item->to_string() .", $value\n" if !$opts{d};
        $req->setValue($item->asn(), $value);
      }
      else
      {
        print STDERR "DEBUG: not found $oid\n" if $opts{d};
        $req->setError($reqi, SNMP_ERR_NOSUCHNAME);
      }
    }
    elsif ($reqi->getMode() == MODE_GETNEXT)
    {
      print STDERR "DEBUG: MODE is NEXT\n" if $opts{d};
      my $item = $app->getNextItem($oid);
      if($item)
      {
        my $value = defined $item->value() ? $item->value() : '';
        print STDERR "DEBUG:    " .$item->to_string() .", $value\n" if $opts{d};

        print STDERR  $item->to_string() .", $value\n" if !$opts{d};
        $req->setOID($item->oid());
        $req->setValue($item->asn(), $value);
      }
      else
      {
        print STDERR "DEBUG: not found $oid\n" if $opts{d};
        $req->setError($reqi, SNMP_ERR_NOSUCHNAME);
      }
    }
    elsif ($reqi->getMode() == MODE_SET_ACTION)
    {
      print STDERR "DEBUG: MODE is SET\n" if $opts{d};
      my $item = $app->getItem($oid);

      if($item)
      {
        my $cb = $app->writableItem($oid);
        if($cb)
        {
          my $value = $req->getValue();

          print STDERR "DEBUG: updating db, $oid -> $value\n" if $opts{d};
          # update the DB
          if(!$cb->cb_set($oid, $value))
          {
            syslog('err', "ERROR: failed to set value: $oid (value: $value)");
            $req->setError($reqi, SNMP_ERR_WRONGVALUE);
            next;
          }

          print STDERR "DEBUG: updating cache, $oid -> $value\n" if $opts{d};
          $app->setItem(OpenSistemas::OID::Item->new({
            oid => $item->oid(),
            asn => $item->asn(),
            value => $value,
            eventid => $item->eventid()
          }));
        }
        else
        {
          print STDERR "DEBUG: read only $oid\n" if $opts{d};
          syslog('info', "SET action request on a RO field: $oid");
          $req->setError($reqi, SNMP_ERR_READONLY);
        }
      }
      else
      {
        print STDERR "DEBUG: not found $oid\n" if $opts{d};
        $req->setError($reqi, SNMP_ERR_NOSUCHNAME);
      }
    }
  }

  print STDERR "DEBUG: leaving snmpHandler...\n" if $opts{d};
}

sub terminate
{
  $app->end();
  syslog('info', "TERM caught, stopping");
}

exit 1 if !getopts('hdp:', \%opts);

if ($opts{h})
{
    print $usagetxt;
    exit 0;
}

if(@ARGV ne 1)
{
    print STDERR "ERROR: missing argument, try -h\n" if @ARGV ne 1;
    exit 1;
}

my $pidFile = "/tmp/$PROG.PID";
$pidFile = $opts{p} if $opts{p};

if(-f $pidFile)
{
    print STDERR "ERROR: service already running, check $pidFile\n";
    exit 1;
}

my $conf = YAML::LoadFile($ARGV[0]);

if(!$conf)
{
    printf STDERR "ERROR: loading $ARGV[0]\n";
    exit 1;
}

if(!$conf->{zabbix} || !$conf->{zabbix}->{dsn})
{
    printf STDERR "ERROR: you must provide a valid DSN string for your database\n";
    exit 1;
}

if(!$conf->{zabbix} || !$conf->{zabbix}->{username} || !$conf->{zabbix}->{password})
{
    printf STDERR "ERROR: you must provide username/password to access your database\n";
    exit 1;
}

my $agentName = $PROG .'.agent';
print STDERR 'DEBUG: agent name is ' .$agentName ."\n" if $opts{d};

$app = OpenSistemas::OSZSNMP->new($agentName);
$app->pollTime($conf->{pollTime}) if $conf->{pollTime};

print STDERR "DEBUG: polling every " .$app->pollTime() ." seconds\n" if $opts{d};
openlog($PROG, 'pid', 'daemon');

syslog('info', "Starting $PROG v$VERSION, conf is $ARGV[0], pidfile is $pidFile");

if(!$opts{d})
{
  my $pid;

  # daemonize
  local $SIG{'HUP'} = 'IGNORE';
  if ($pid = fork())
  {
    open(PID, ">$pidFile");
    print(PID "$pid");
    close(PID);
    exit 0;
  }

  close(STDIN);
  close(STDOUT),
  close(STDERR);
  open(STDIN,  "+>/dev/null");
  open(STDOUT, "+>&STDIN");
  open(STDERR, "+>&STDIN");
}


$SIG{'TERM'} = 'terminate';
$app->registerAgent(\&snmpHandler);

print STDERR 'DEBUG: trying to connect dbi:mysql:' .$conf->{zabbix}->{dsn}
    .', user: ' .$conf->{zabbix}->{username} ."\n" if $opts{d};

my $dbh = DBI->connect('dbi:mysql:' .$conf->{zabbix}->{dsn},
    $conf->{zabbix}->{username},
    $conf->{zabbix}->{password},
    {'RaiseError' => 0});

if(!$dbh)
{
  syslog('err', 'Database connection failed, terminating');
  $app->disconnectAgent();
  closelog();
  unlink($pidFile);
  exit 1;
}

$app->connection($dbh);

print STDERR "DEBUG: performing first update...\n" if $opts{d};
$app->update();

# poll for changes
$SIG{'ALRM'} = 'pollData';
alarm $app->{pollTime};

print STDERR "DEBUG: entering the loop\n" if $opts{d};
while($app->running())
{
  $app->loop();
}

print STDERR "DEBUG: shutdown in process...\n" if $opts{d};
$app->disconnectAgent();
$app->disconnect();

unlink($pidFile);

syslog('info', "cleanning done, bye");

closelog();

exit 0;

=pod

=head1 NAME

oszsnmpd - daemon of the SNMP poll interface for Zabbix

=head1 DESCRIPTION

This is the daemon that provides a SNMP poll interface for Zabbix. Currently it's
implemented as a sub-agent of B<snmpd>, and that implies:

=over

=item *

You have B<snmpd> installed.

=item *

You must run B<oszsnmpd> with enough privileges to be attached to B<snmpd>.
That usually means you must run oszsnmpd as root.

=back

By default B<snmpd> will try to load C</etc/snmp/oszsnmpd.agent.conf> configuration file, and
add its content to the default rules in the default C<snmpd.conf> file. Using this file is optional.

Sub-agent support must be enabled in B<snmpd>, including C<master agentx> in C<snmpd.conf>.

Please notice that you must correctly setup B<snmpd> in order to access to the sub-agent OIDs.

An example of minimal C<snmpd.conf> file could be:

    com2sec readonly  default   public
    com2sec readwrite default   private

    group MyROGroup v2c         readonly
    group MyRWGroup v2c         readwrite

    view all    included  .1                             80
    view oszrw  included  .1.3.6.1.4.1.35633.1.1.3.1.1.5

    access MyROGroup ""  any  noauth  exact  all  none   none
    access MyRWGroup ""  any  noauth  exact  all  oszrw  none

    master  agentx

Despite that B<oszsnmpd> won't allow B<SET> operations in read-only OIDs, it's a good practice to limit
that kind of access using C<snmpd.conf> directives.

B<oszsnmpd> uses SNMP version 2c.

=head1 USAGE

osztrapd [OPTIONS] oszsnmpd.conf

=over

=item -h

help screen

=item -p file

PID file (default: /tmp/oszsnmpd.PID)

=item -d

enable debug (very verbose, attached to the console)

=back

=head1 CONFIGURATION

osztrapd uses the YAML format for its configuration file:

    -- mandatory, database connection data
    zabbix:
      dsn: database=<database>[;host=<hostname>[;port=<port>]]
      username: <username>
      password: <passowed>

    -- optional, default: 60 seconds
    pollTime: <seconds>

Example:

    zabbix:
      dsn: database=zabbix
      username: uzabbix
      password: secret

=head1 TESTING

The easiest way to test your installation is using B<snmpwalk> locally:

    snmpwalk -m mibs/OSZSNMP-MIB.txt -v 2c -c public 127.0.0.1 .1.3.6.1.4.1.35633

That command will dump all the SNMP tree published by B<oszsnmpd>.

=head1 CAVEATS

Current Perl bindings for NetSNMP agent doesn't support B<OPAQUE> and, although it's used
in the MIB to represent floating-point numbers, B<oszsnmpd> actually uses a string for these
types.

For some reason, if the communication between the sub-agent and B<snmpd> is too fast, the
sub-agent gets disconnected. B<oszsnmpd> it's using a workaround to avoid this problem, but
we believe there's a bug in either C<snmpd>, the SNMP library, or the Perl bindings.

=head1 AUTHOR

This script has been written by Juan J. Martinez <jjmartinez@opensistemas.com>.

=head1 LICENSE

This application it's free software and it's published under the GPL3 license.

You should have received a copy of the GNU General Public License along with this program.
If not, see <http://www.gnu.org/licenses/>.

=head1 SEE ALSO

osztrapd(1), NetSNMP::agent(3pm), OSZSNMP-MIB.

Project website: http://snmp4zabbix.opensistemas.com/

=cut
